This notebook is all about benchmarking some R code used in this package.

Hardware / Software used:

  • Intel i7-4600U
  • Compilation flags for C/C++: -O2 -Wall $(DEBUGFLAG) -mtune=core2 (R’s defaults)
  • Windows Server 2012 R2
  • R 3.3.2 + Intel MKL

Libraries

library(data.table)
data.table 1.10.4
  The fastest way to learn (by data.table authors): https://www.datacamp.com/courses/data-analysis-the-data-table-way
  Documentation: ?data.table, example(data.table) and browseVignettes("data.table")
  Release notes, videos and slides: http://r-datatable.com
library(microbenchmark)
library(Rcpp)
library(ggplot2)
library(plotly)

Attaching package: <U+393C><U+3E31>plotly<U+393C><U+3E32>

The following object is masked from <U+393C><U+3E31>package:ggplot2<U+393C><U+3E32>:

    last_plot

The following object is masked from <U+393C><U+3E31>package:stats<U+393C><U+3E32>:

    filter

The following object is masked from <U+393C><U+3E31>package:graphics<U+393C><U+3E32>:

    layout
# Helper function to print data well in tables
print_well <- function(data, digits = 6) {
  
  # To milliseconds
  data <- data / 1000000
  
  # Sprintf helper
  sprintf_helper <- paste0("%.0", digits, "f")
  
  cat("| Min | 25% | 50% | 75% | Max | Mean |  \n| --: | --: | --: | --: | --: | --: |  \n| ", sprintf(sprintf_helper, min(data)), " | ", sprintf(sprintf_helper, quantile(data, probs = 0.25)), " | ", sprintf(sprintf_helper, median(data)), " | ", sprintf(sprintf_helper, quantile(data, probs = 0.75)), " | ", sprintf(sprintf_helper, max(data)), " | ", sprintf(sprintf_helper, mean(data)), " |  \n", sep = "")
  
  return(data)
  
}
# Test case function
# Arguments renamed to avoid recursive clash
test_case <- function(f, preds, labels, eps) {
  cat("Test case: ", paste(do.call(f, list(preds = preds[1:50],
                                           labels = labels[1:5],
                                           eps = 1e-15)), collapse = ", "), "  \n", sep = "")
}
# Fastest Logloss function
cppFunction("double Lpp_logloss1(NumericVector preds, NumericVector labels, double eps) {
  NumericVector clamped = clamp(eps, preds, 1 - eps);
  NumericVector loggy = -log(1 - clamped);
  double logloss = sum(loggy) / loggy.size();
  return logloss;
}")
# Fastest Transformer function
cppFunction("NumericVector Lpp_vec2mat2vec(NumericVector preds, NumericVector labels) {
  int labels_size = labels.size();
  NumericVector selected(labels_size);
  selected = (preds.size() / labels_size) * seq(0, labels_size - 1);
  selected = selected + labels;
  NumericVector to_return(labels_size);
  to_return = preds[selected];
  return to_return;
}")

Benchmarking Clamped Vector to Logloss

For a 10-class vector of 1,000,000 observations:

  • Vector A of length=(1000000 * 10)
  • Vector B of length=(1000000) with 10 classes
A = [1:1, 1:2, 1:3, 1:4... 1:10, 2:1, 2:2, 2:3..., 1000000:8, 1000000:9, 1000000:10]
B = [3, 5, 9, 1, 4, 8, 6, ...]

Get the following Vector C, D, and E:

C = [1:4, 2:6, 3:10, 4:2, 5:5, 6:9, 7:7, ...]
D = Clamped C by 1e-15
E = Mean of logloss(D, B)

Initialize data

# Generate random data
set.seed(11111)
data <- runif(10000000, 0, 1)
labels <- round(runif(1000000, 0, 9), digits = 0)
# Background truth example (no clamping though)
array(data[1:50], dim = c(10, 5))
           [,1]       [,2]       [,3]       [,4]       [,5]
 [1,] 0.5014483 0.57219649 0.70236924 0.06440384 0.78924978
 [2,] 0.9702328 0.34292525 0.50889166 0.65780657 0.72449914
 [3,] 0.7876004 0.09627503 0.20268701 0.19973930 0.00886554
 [4,] 0.9022259 0.74235690 0.90706612 0.24664551 0.57348710
 [5,] 0.8141778 0.42274539 0.05441064 0.25997440 0.73034459
 [6,] 0.7998922 0.98402494 0.09045308 0.14869691 0.42458612
 [7,] 0.1158690 0.89258437 0.85759800 0.21375685 0.07684022
 [8,] 0.7171363 0.59541565 0.94780036 0.20598170 0.53318628
 [9,] 0.1020639 0.80531234 0.16479666 0.02075780 0.28521238
[10,] 0.6856938 0.42102379 0.42536097 0.63880218 0.86408083
labels[1:5]
[1] 5 1 1 7 8
data[c(1 + labels[1], 11 + labels[2], 21 + labels[3], 31 + labels[4], 41 + labels[5])]
[1] 0.7998922 0.3429253 0.5088917 0.2059817 0.2852124
-log(1 - data[c(1 + labels[1], 11 + labels[2], 21 + labels[3], 31 + labels[4], 41 + labels[5])])
[1] 1.6088991 0.4199575 0.7110905 0.2306488 0.3357698
mean(-log(1 - data[c(1 + labels[1], 11 + labels[2], 21 + labels[3], 31 + labels[4], 41 + labels[5])]))
[1] 0.6612731
# How many digits for benchmarking in milliseconds
my_digits <- 6L
# How many runs for benchmarking?
my_runs <- 1000L

Benchmarks

# ===== BLOCK 1 =====
faster1 <- function(preds, labels, eps = 1e-15) {
  temp_preds <- Lpp_vec2mat2vec(preds, labels)
  temp_log <- Lpp_logloss1(temp_preds, labels, eps)
  return(temp_log)
}
test_case(faster1, preds = data, labels = labels, eps = 1e-15)

Test case: 0.661273148685013

data1 <- print_well(microbenchmark(faster1(data, labels), times = my_runs)$time, digits = my_digits)
Min 25% 50% 75% Max Mean
68.776904 74.052150 77.595495 80.546207 158.218773 79.329577
# ===== BLOCK 2 =====
faster2 <- function(preds, labels, eps = 1e-15) {
  x <- pmin(pmax(preds[((0:(length(labels) - 1)) * (length(preds) / length(labels))) + labels + 1], eps), 1 - eps)
  return(-mean(log(1 - x)))
}
test_case(faster2, preds = data, labels = labels, eps = 1e-15)

Test case: 0.661273148685013

data2 <- print_well(microbenchmark(faster2(data, labels), times = my_runs)$time, digits = my_digits)
Min 25% 50% 75% Max Mean
76.919802 83.673872 87.017170 90.827465 168.569882 90.820915
# ===== BLOCK 3 =====
faster3 <- function(preds, labels, eps = 1e-15) {
  x <- pmin(pmax(preds[((0:(length(labels) - 1)) * (length(preds) / length(labels))) + labels + 1], eps), 1 - eps)
  return(-sum(log(1 - x)) / length(labels))
}
test_case(faster3, preds = data, labels = labels, eps = 1e-15)

Test case: 0.661273148685013

data3 <- print_well(microbenchmark(faster3(data, labels), times = my_runs)$time, digits = my_digits)
Min 25% 50% 75% Max Mean
76.257605 83.326427 85.514111 88.191601 170.386934 90.041468
# ===== BLOCK 4 =====
cppFunction("double faster4(NumericVector preds, NumericVector labels, double eps) {
  int labels_size = labels.size();
  NumericVector selected(labels_size);
  selected = (preds.size() / labels_size) * seq(0, labels_size - 1);
  selected = selected + labels;
  NumericVector to_return(labels_size);
  to_return = preds[selected];
  NumericVector clamped = clamp(eps, to_return, 1 - eps);
  NumericVector loggy = -(log(1 - clamped));
  double logloss = sum(loggy) / labels_size;
  return logloss;
}")
test_case(faster4, preds = data, labels = labels, eps = 1e-15)

Test case: 0.661273148685013

data4 <- print_well(microbenchmark(faster4(data, labels, eps = 1e-15), times = my_runs)$time, digits = my_digits)
Min 25% 50% 75% Max Mean
68.158803 70.104625 79.027087 81.212965 155.654754 77.006338

Summary Results

data_time <- data.table(rbindlist(list(data.frame(Time = data1, Bench = "faster1"),
                                       data.frame(Time = data2, Bench = "faster2"),
                                       data.frame(Time = data3, Bench = "faster3"),
                                       data.frame(Time = data4, Bench = "faster4"))))
data_time <- data_time[, t_mean := mean(Time), by = Bench]
data_time <- data_time[, t_median := median(Time), by = Bench]
data_time$Benchs <- data_time$Bench 
levels(data_time$Benchs) <- paste0("faster", 1:4, "= [", sprintf(paste0("%.0", my_digits, "f"), data_time[, list(min(Time)), by = Bench]$V1), ", ", sprintf(paste0("%.0", my_digits, "f"), data_time[, list(max(Time)), by = Bench]$V1), "], mean=", sprintf(paste0("%.0", my_digits, "f"), data_time[, list(mean(Time)), by = Bench]$V1), ", median=", sprintf(paste0("%.0", my_digits, "f"), data_time[, list(median(Time)), by = Bench]$V1))
my_time <- data_time[, list(min(Time), quantile(Time, probs = 0.25), median(Time), quantile(Time, probs = 0.75), max(Time), mean(Time)), by = Bench]
colnames(my_time) <- c("Function", "Min", "25%", "50%", "75%", "Max", "Mean")
my_time <- my_time[order(Mean, decreasing = FALSE), ]
print(my_time, digits = 6)

Plot Results

ggplotly(ggplot(data = data_time, aes(x = Time)) + geom_histogram(aes(y = ..density..), bins = 20, color = "darkblue", fill = "lightblue") + facet_wrap(~ Benchs, ncol = 2) + geom_vline(aes(xintercept = t_mean), color = "blue", linetype = "dashed", size = 2) + geom_vline(aes(xintercept = t_median), color = "red", linetype = "dashed", size = 2) + labs(x = "Time (Milliseconds)", y = "Density") + theme_bw())
ggplotly(ggplot(data = data_time[, .(Time, Bench)], aes(x = Time, y = ..count.., fill = Bench)) + geom_histogram(aes(y = ..density..), bins = 100, position = "fill") + labs(x = "Time (Milliseconds)", y = "Density") + theme_bw())
ggplotly(ggplot(data = data_time[, .(Time, Bench)], aes(x = Time, y = ..count.., fill = Bench)) + geom_density(position = "fill") + labs(x = "Time (Milliseconds)", y = "Density") + theme_bw())

Scaling Benchmarks

Benchmarker <- function(f, size, runs, digits, name) {
  
  data_runs <- list()
  
  for (i in 1:length(size)) {
    set.seed(11111)
    data <- runif(size[i] * 10, 0, 1)
    labels <- round(runif(size[i], 0, 9), digits = 0)
    cat("  \n  \n## ", name, " run: ",format(size[i], big.mark = ",", scientific = FALSE), " samples (", format(runs[i], big.mark = ",", scientific = FALSE), " times)  \n  \n", sep = "")
    test_case(f, preds = data, labels = labels, eps = 1e-15)
    cat("  \n")
    data_runs[[i]] <- print_well(microbenchmark(f(data, labels, eps = 1e-15), times = runs[i])$time, digits = digits)
    data_runs[[i]] <- data.table(Bench = as.factor(paste0("[", i, "] ", format(size[i], big.mark = ",", scientific = FALSE))), Function = as.factor(name), Time = data_runs[[i]])
    gc(verbose = FALSE)
  }
  
  return(data_runs)
  
}
bench_size <- c(100, 1000, 10000, 100000, 1000000, 10000000)
bench_runs <- c(10000, 5000, 1000, 500, 100, 50)
run1 <- Benchmarker(faster3, bench_size, bench_runs, my_digits, "Pure R")

Pure R run: 100 samples (10,000 times)

Test case: 1.35212558439888

Min 25% 50% 75% Max Mean
0.022808 0.025469 0.027370 0.043335 4.638802 0.036441

Pure R run: 1,000 samples (5,000 times)

Test case: 1.11833540263107

Min 25% 50% 75% Max Mean
0.073366 0.076787 0.083630 0.091613 3.864085 0.088550

Pure R run: 10,000 samples (1,000 times)

Test case: 0.797706755821707

Min 25% 50% 75% Max Mean
0.586930 0.595673 0.670181 0.683865 3.591907 0.670329

Pure R run: 100,000 samples (500 times)

Test case: 1.90925095682509

Min 25% 50% 75% Max Mean
6.245258 6.728032 7.275997 7.517289 29.690539 7.664803

Pure R run: 1,000,000 samples (100 times)

Test case: 0.661273148685013

Min 25% 50% 75% Max Mean
64.671813 67.926919 71.717638 75.255851 151.530276 74.316310

Pure R run: 10,000,000 samples (50 times)

Test case: 0.966169328545996

Min 25% 50% 75% Max Mean
741.884853 786.242472 809.841423 821.969478 993.530082 812.830624
run2 <- Benchmarker(faster4, bench_size, bench_runs, my_digits, "Rcpp")

Rcpp run: 100 samples (10,000 times)

Test case: 1.35212558439888

Min 25% 50% 75% Max Mean
0.006082 0.006463 0.006843 0.007223 0.068045 0.007184

Rcpp run: 1,000 samples (5,000 times)

Test case: 1.11833540263107

Min 25% 50% 75% Max Mean
0.051318 0.055120 0.060062 0.067284 0.210595 0.064723

Rcpp run: 10,000 samples (1,000 times)

Test case: 0.797706755821707

Min 25% 50% 75% Max Mean
0.510143 0.515369 0.583890 0.593773 1.034731 0.574722

Rcpp run: 100,000 samples (500 times)

Test case: 1.90925095682509

Min 25% 50% 75% Max Mean
5.590283 5.684082 5.804680 6.427818 12.891180 6.154242

Rcpp run: 1,000,000 samples (100 times)

Test case: 0.661273148685013

Min 25% 50% 75% Max Mean
57.931618 59.823270 62.286268 66.095328 138.984261 63.797930

Rcpp run: 10,000,000 samples (50 times)

Test case: 0.966169328545996

Min 25% 50% 75% Max Mean
673.112510 692.100884 695.991768 706.077447 800.154031 702.179116

Scaling Results

run1_all <- rbindlist(run1)
run2_all <- rbindlist(run2)
run_all <- rbind(run1_all, run2_all)
run_all$Repeats <- rep(inverse.rle(list(lengths = bench_runs, values = bench_size)), 2)
run_all$MilObs <- (1000 / run_all$Time) * run_all$Repeats / 1000000
run_time <- run_all[, list(quantile(Time, probs = 0.05), median(Time), quantile(Time, probs = 0.95), mean(Time)), by = list(Function, Bench)]
colnames(run_time) <- c("Function", "Benchmark", "5%", "50%", "95%", "Mean")
run_time$`Mil.Obs/s` <- (1000 / run_time$Mean) * bench_size / 1000000
run_time$`5%` <- format(run_time$`5%`, digits = 6, scientific = FALSE)
run_time$`50%` <- format(run_time$`50%`, digits = 6, scientific = FALSE)
run_time$`95%` <- format(run_time$`95%`, digits = 6, scientific = FALSE)
run_time$Mean <- format(run_time$Mean, digits = 6, scientific = FALSE)
run_time$`Mil.Obs/s` <- format(run_time$`Mil.Obs/s`, digits = 6, scientific = FALSE)
print(run_time[1:(nrow(run_time) / 2)])
print(run_time[(nrow(run_time) / 2 + 1):nrow(run_time)])
ggplot(run_all, aes(x = Bench, y = Time, color = Function, fill = Bench)) + geom_boxplot() + scale_y_log10(labels = scales::comma, breaks = c(0.01, 0.1, 1, 10, 100, 1000, 10000)) + stat_summary(fun.y = mean, geom = "line", aes(group = Function)) + stat_summary(fun.y = mean, geom = "point", aes(group = Function)) + labs(x = "Benchmark", y = "Time (Milliseconds)") + theme_bw()

ggplot(run_all, aes(x = Bench, y = MilObs, color = Function, fill = Bench)) + geom_boxplot() + scale_y_log10(labels = scales::comma, breaks = c(1, 2.5, 5, 7.5, 10, 12.5, 15, 17.5, 20, 22.5, 25), limits = c(1, NA)) + stat_summary(fun.y = mean, geom = "line", aes(group = Function)) + stat_summary(fun.y = mean, geom = "point", aes(group = Function)) + labs(x = "Benchmark", y = "Throughput (Million Obs./s)") + theme_bw()

ggplot(data = run_all, aes(x = MilObs, color = Function, fill = Function, group = Function)) + coord_flip() + stat_ecdf(aes(ymin = ..y.., ymax = 1), alpha = 0.5, geom = "ribbon") + stat_ecdf(geom = "line", size = 2, alpha = 0.75, pad = FALSE) + labs(x = "Throughput (Million Obs./s)", y = "Percentile") + facet_wrap(~ Bench, dir = "h", ncol = 2, scales = "free") + theme_bw()

LS0tDQp0aXRsZTogIkJlbmNobWFya3M6IE11bHRpLWNsYXNzIExvZ2xvc3MiDQpvdXRwdXQ6DQogIGh0bWxfbm90ZWJvb2s6DQogICAgY29sbGFwc2VkOiBubw0KICAgIHRoZW1lOiB1bml0ZWQNCiAgICB0b2M6IHllcw0KICAgIHRvY19kZXB0aDogMQ0KICAgIHRvY19mbG9hdDogeWVzDQotLS0NCg0KVGhpcyBub3RlYm9vayBpcyBhbGwgYWJvdXQgYmVuY2htYXJraW5nIHNvbWUgUiBjb2RlIHVzZWQgaW4gdGhpcyBwYWNrYWdlLg0KDQpIYXJkd2FyZSAvIFNvZnR3YXJlIHVzZWQ6DQoNCiogSW50ZWwgaTctNDYwMFUNCiogQ29tcGlsYXRpb24gZmxhZ3MgZm9yIEMvQysrOiBgLU8yIC1XYWxsICQoREVCVUdGTEFHKSAtbXR1bmU9Y29yZTJgIChSJ3MgZGVmYXVsdHMpDQoqIFdpbmRvd3MgU2VydmVyIDIwMTIgUjINCiogUiAzLjMuMiArIEludGVsIE1LTA0KDQojIExpYnJhcmllcw0KDQpgYGB7ciBpbml0fQ0KbGlicmFyeShkYXRhLnRhYmxlKQ0KbGlicmFyeShtaWNyb2JlbmNobWFyaykNCmxpYnJhcnkoUmNwcCkNCmxpYnJhcnkoZ2dwbG90MikNCmxpYnJhcnkocGxvdGx5KQ0KYGBgDQoNCmBgYHtyIGJhc2VkfQ0KDQojIEhlbHBlciBmdW5jdGlvbiB0byBwcmludCBkYXRhIHdlbGwgaW4gdGFibGVzDQpwcmludF93ZWxsIDwtIGZ1bmN0aW9uKGRhdGEsIGRpZ2l0cyA9IDYpIHsNCiAgDQogICMgVG8gbWlsbGlzZWNvbmRzDQogIGRhdGEgPC0gZGF0YSAvIDEwMDAwMDANCiAgDQogICMgU3ByaW50ZiBoZWxwZXINCiAgc3ByaW50Zl9oZWxwZXIgPC0gcGFzdGUwKCIlLjAiLCBkaWdpdHMsICJmIikNCiAgDQogIGNhdCgifCBNaW4gfCAyNSUgfCA1MCUgfCA3NSUgfCBNYXggfCBNZWFuIHwgIFxufCAtLTogfCAtLTogfCAtLTogfCAtLTogfCAtLTogfCAtLTogfCAgXG58ICIsIHNwcmludGYoc3ByaW50Zl9oZWxwZXIsIG1pbihkYXRhKSksICIgfCAiLCBzcHJpbnRmKHNwcmludGZfaGVscGVyLCBxdWFudGlsZShkYXRhLCBwcm9icyA9IDAuMjUpKSwgIiB8ICIsIHNwcmludGYoc3ByaW50Zl9oZWxwZXIsIG1lZGlhbihkYXRhKSksICIgfCAiLCBzcHJpbnRmKHNwcmludGZfaGVscGVyLCBxdWFudGlsZShkYXRhLCBwcm9icyA9IDAuNzUpKSwgIiB8ICIsIHNwcmludGYoc3ByaW50Zl9oZWxwZXIsIG1heChkYXRhKSksICIgfCAiLCBzcHJpbnRmKHNwcmludGZfaGVscGVyLCBtZWFuKGRhdGEpKSwgIiB8ICBcbiIsIHNlcCA9ICIiKQ0KICANCiAgcmV0dXJuKGRhdGEpDQogIA0KfQ0KDQojIFRlc3QgY2FzZSBmdW5jdGlvbg0KIyBBcmd1bWVudHMgcmVuYW1lZCB0byBhdm9pZCByZWN1cnNpdmUgY2xhc2gNCnRlc3RfY2FzZSA8LSBmdW5jdGlvbihmLCBwcmVkcywgbGFiZWxzLCBlcHMpIHsNCiAgY2F0KCJUZXN0IGNhc2U6ICIsIHBhc3RlKGRvLmNhbGwoZiwgbGlzdChwcmVkcyA9IHByZWRzWzE6NTBdLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxhYmVscyA9IGxhYmVsc1sxOjVdLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGVwcyA9IDFlLTE1KSksIGNvbGxhcHNlID0gIiwgIiksICIgIFxuIiwgc2VwID0gIiIpDQp9DQoNCiMgRmFzdGVzdCBMb2dsb3NzIGZ1bmN0aW9uDQpjcHBGdW5jdGlvbigiZG91YmxlIExwcF9sb2dsb3NzMShOdW1lcmljVmVjdG9yIHByZWRzLCBOdW1lcmljVmVjdG9yIGxhYmVscywgZG91YmxlIGVwcykgew0KICBOdW1lcmljVmVjdG9yIGNsYW1wZWQgPSBjbGFtcChlcHMsIHByZWRzLCAxIC0gZXBzKTsNCiAgTnVtZXJpY1ZlY3RvciBsb2dneSA9IC1sb2coMSAtIGNsYW1wZWQpOw0KICBkb3VibGUgbG9nbG9zcyA9IHN1bShsb2dneSkgLyBsb2dneS5zaXplKCk7DQogIHJldHVybiBsb2dsb3NzOw0KfSIpDQoNCiMgRmFzdGVzdCBUcmFuc2Zvcm1lciBmdW5jdGlvbg0KY3BwRnVuY3Rpb24oIk51bWVyaWNWZWN0b3IgTHBwX3ZlYzJtYXQydmVjKE51bWVyaWNWZWN0b3IgcHJlZHMsIE51bWVyaWNWZWN0b3IgbGFiZWxzKSB7DQogIGludCBsYWJlbHNfc2l6ZSA9IGxhYmVscy5zaXplKCk7DQogIE51bWVyaWNWZWN0b3Igc2VsZWN0ZWQobGFiZWxzX3NpemUpOw0KICBzZWxlY3RlZCA9IChwcmVkcy5zaXplKCkgLyBsYWJlbHNfc2l6ZSkgKiBzZXEoMCwgbGFiZWxzX3NpemUgLSAxKTsNCiAgc2VsZWN0ZWQgPSBzZWxlY3RlZCArIGxhYmVsczsNCiAgTnVtZXJpY1ZlY3RvciB0b19yZXR1cm4obGFiZWxzX3NpemUpOw0KICB0b19yZXR1cm4gPSBwcmVkc1tzZWxlY3RlZF07DQogIHJldHVybiB0b19yZXR1cm47DQp9IikNCg0KYGBgDQoNCiMgQmVuY2htYXJraW5nIENsYW1wZWQgVmVjdG9yIHRvIExvZ2xvc3MNCg0KRm9yIGEgMTAtY2xhc3MgdmVjdG9yIG9mIDEsMDAwLDAwMCBvYnNlcnZhdGlvbnM6DQoNCiogVmVjdG9yIEEgb2YgbGVuZ3RoPSgxMDAwMDAwICogMTApDQoqIFZlY3RvciBCIG9mIGxlbmd0aD0oMTAwMDAwMCkgd2l0aCAxMCBjbGFzc2VzDQoNCmBgYA0KQSA9IFsxOjEsIDE6MiwgMTozLCAxOjQuLi4gMToxMCwgMjoxLCAyOjIsIDI6My4uLiwgMTAwMDAwMDo4LCAxMDAwMDAwOjksIDEwMDAwMDA6MTBdDQpCID0gWzMsIDUsIDksIDEsIDQsIDgsIDYsIC4uLl0NCmBgYA0KDQpHZXQgdGhlIGZvbGxvd2luZyBWZWN0b3IgQywgRCwgYW5kIEU6DQoNCmBgYA0KQyA9IFsxOjQsIDI6NiwgMzoxMCwgNDoyLCA1OjUsIDY6OSwgNzo3LCAuLi5dDQpEID0gQ2xhbXBlZCBDIGJ5IDFlLTE1DQpFID0gTWVhbiBvZiBsb2dsb3NzKEQsIEIpDQpgYGANCg0KIyBJbml0aWFsaXplIGRhdGENCg0KYGBge3IgYmVuY2gxfQ0KIyBHZW5lcmF0ZSByYW5kb20gZGF0YQ0Kc2V0LnNlZWQoMTExMTEpDQpkYXRhIDwtIHJ1bmlmKDEwMDAwMDAwLCAwLCAxKQ0KbGFiZWxzIDwtIHJvdW5kKHJ1bmlmKDEwMDAwMDAsIDAsIDkpLCBkaWdpdHMgPSAwKQ0KDQojIEJhY2tncm91bmQgdHJ1dGggZXhhbXBsZSAobm8gY2xhbXBpbmcgdGhvdWdoKQ0KYXJyYXkoZGF0YVsxOjUwXSwgZGltID0gYygxMCwgNSkpDQpsYWJlbHNbMTo1XQ0KZGF0YVtjKDEgKyBsYWJlbHNbMV0sIDExICsgbGFiZWxzWzJdLCAyMSArIGxhYmVsc1szXSwgMzEgKyBsYWJlbHNbNF0sIDQxICsgbGFiZWxzWzVdKV0NCi1sb2coMSAtIGRhdGFbYygxICsgbGFiZWxzWzFdLCAxMSArIGxhYmVsc1syXSwgMjEgKyBsYWJlbHNbM10sIDMxICsgbGFiZWxzWzRdLCA0MSArIGxhYmVsc1s1XSldKQ0KbWVhbigtbG9nKDEgLSBkYXRhW2MoMSArIGxhYmVsc1sxXSwgMTEgKyBsYWJlbHNbMl0sIDIxICsgbGFiZWxzWzNdLCAzMSArIGxhYmVsc1s0XSwgNDEgKyBsYWJlbHNbNV0pXSkpDQoNCiMgSG93IG1hbnkgZGlnaXRzIGZvciBiZW5jaG1hcmtpbmcgaW4gbWlsbGlzZWNvbmRzDQpteV9kaWdpdHMgPC0gNkwNCg0KIyBIb3cgbWFueSBydW5zIGZvciBiZW5jaG1hcmtpbmc/DQpteV9ydW5zIDwtIDEwMDBMDQpgYGANCg0KIyBCZW5jaG1hcmtzDQoNCmBgYHtyIGJlbmNoMiwgcmVzdWx0cz0iYXNpcyJ9DQoNCiMgPT09PT0gQkxPQ0sgMSA9PT09PQ0KZmFzdGVyMSA8LSBmdW5jdGlvbihwcmVkcywgbGFiZWxzLCBlcHMgPSAxZS0xNSkgew0KICB0ZW1wX3ByZWRzIDwtIExwcF92ZWMybWF0MnZlYyhwcmVkcywgbGFiZWxzKQ0KICB0ZW1wX2xvZyA8LSBMcHBfbG9nbG9zczEodGVtcF9wcmVkcywgbGFiZWxzLCBlcHMpDQogIHJldHVybih0ZW1wX2xvZykNCn0NCnRlc3RfY2FzZShmYXN0ZXIxLCBwcmVkcyA9IGRhdGEsIGxhYmVscyA9IGxhYmVscywgZXBzID0gMWUtMTUpDQpkYXRhMSA8LSBwcmludF93ZWxsKG1pY3JvYmVuY2htYXJrKGZhc3RlcjEoZGF0YSwgbGFiZWxzKSwgdGltZXMgPSBteV9ydW5zKSR0aW1lLCBkaWdpdHMgPSBteV9kaWdpdHMpDQoNCiMgPT09PT0gQkxPQ0sgMiA9PT09PQ0KZmFzdGVyMiA8LSBmdW5jdGlvbihwcmVkcywgbGFiZWxzLCBlcHMgPSAxZS0xNSkgew0KICB4IDwtIHBtaW4ocG1heChwcmVkc1soKDA6KGxlbmd0aChsYWJlbHMpIC0gMSkpICogKGxlbmd0aChwcmVkcykgLyBsZW5ndGgobGFiZWxzKSkpICsgbGFiZWxzICsgMV0sIGVwcyksIDEgLSBlcHMpDQogIHJldHVybigtbWVhbihsb2coMSAtIHgpKSkNCn0NCnRlc3RfY2FzZShmYXN0ZXIyLCBwcmVkcyA9IGRhdGEsIGxhYmVscyA9IGxhYmVscywgZXBzID0gMWUtMTUpDQpkYXRhMiA8LSBwcmludF93ZWxsKG1pY3JvYmVuY2htYXJrKGZhc3RlcjIoZGF0YSwgbGFiZWxzKSwgdGltZXMgPSBteV9ydW5zKSR0aW1lLCBkaWdpdHMgPSBteV9kaWdpdHMpDQoNCiMgPT09PT0gQkxPQ0sgMyA9PT09PQ0KZmFzdGVyMyA8LSBmdW5jdGlvbihwcmVkcywgbGFiZWxzLCBlcHMgPSAxZS0xNSkgew0KICB4IDwtIHBtaW4ocG1heChwcmVkc1soKDA6KGxlbmd0aChsYWJlbHMpIC0gMSkpICogKGxlbmd0aChwcmVkcykgLyBsZW5ndGgobGFiZWxzKSkpICsgbGFiZWxzICsgMV0sIGVwcyksIDEgLSBlcHMpDQogIHJldHVybigtc3VtKGxvZygxIC0geCkpIC8gbGVuZ3RoKGxhYmVscykpDQp9DQp0ZXN0X2Nhc2UoZmFzdGVyMywgcHJlZHMgPSBkYXRhLCBsYWJlbHMgPSBsYWJlbHMsIGVwcyA9IDFlLTE1KQ0KZGF0YTMgPC0gcHJpbnRfd2VsbChtaWNyb2JlbmNobWFyayhmYXN0ZXIzKGRhdGEsIGxhYmVscyksIHRpbWVzID0gbXlfcnVucykkdGltZSwgZGlnaXRzID0gbXlfZGlnaXRzKQ0KDQojID09PT09IEJMT0NLIDQgPT09PT0NCmNwcEZ1bmN0aW9uKCJkb3VibGUgZmFzdGVyNChOdW1lcmljVmVjdG9yIHByZWRzLCBOdW1lcmljVmVjdG9yIGxhYmVscywgZG91YmxlIGVwcykgew0KICBpbnQgbGFiZWxzX3NpemUgPSBsYWJlbHMuc2l6ZSgpOw0KICBOdW1lcmljVmVjdG9yIHNlbGVjdGVkKGxhYmVsc19zaXplKTsNCiAgc2VsZWN0ZWQgPSAocHJlZHMuc2l6ZSgpIC8gbGFiZWxzX3NpemUpICogc2VxKDAsIGxhYmVsc19zaXplIC0gMSk7DQogIHNlbGVjdGVkID0gc2VsZWN0ZWQgKyBsYWJlbHM7DQogIE51bWVyaWNWZWN0b3IgdG9fcmV0dXJuKGxhYmVsc19zaXplKTsNCiAgdG9fcmV0dXJuID0gcHJlZHNbc2VsZWN0ZWRdOw0KICBOdW1lcmljVmVjdG9yIGNsYW1wZWQgPSBjbGFtcChlcHMsIHRvX3JldHVybiwgMSAtIGVwcyk7DQogIE51bWVyaWNWZWN0b3IgbG9nZ3kgPSAtKGxvZygxIC0gY2xhbXBlZCkpOw0KICBkb3VibGUgbG9nbG9zcyA9IHN1bShsb2dneSkgLyBsYWJlbHNfc2l6ZTsNCiAgcmV0dXJuIGxvZ2xvc3M7DQp9IikNCnRlc3RfY2FzZShmYXN0ZXI0LCBwcmVkcyA9IGRhdGEsIGxhYmVscyA9IGxhYmVscywgZXBzID0gMWUtMTUpDQpkYXRhNCA8LSBwcmludF93ZWxsKG1pY3JvYmVuY2htYXJrKGZhc3RlcjQoZGF0YSwgbGFiZWxzLCBlcHMgPSAxZS0xNSksIHRpbWVzID0gbXlfcnVucykkdGltZSwgZGlnaXRzID0gbXlfZGlnaXRzKQ0KDQpgYGANCg0KIyBTdW1tYXJ5IFJlc3VsdHMNCg0KYGBge3IgYmVuY2gzfQ0KDQpkYXRhX3RpbWUgPC0gZGF0YS50YWJsZShyYmluZGxpc3QobGlzdChkYXRhLmZyYW1lKFRpbWUgPSBkYXRhMSwgQmVuY2ggPSAiZmFzdGVyMSIpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZGF0YS5mcmFtZShUaW1lID0gZGF0YTIsIEJlbmNoID0gImZhc3RlcjIiKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRhdGEuZnJhbWUoVGltZSA9IGRhdGEzLCBCZW5jaCA9ICJmYXN0ZXIzIiksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkYXRhLmZyYW1lKFRpbWUgPSBkYXRhNCwgQmVuY2ggPSAiZmFzdGVyNCIpKSkpDQpkYXRhX3RpbWUgPC0gZGF0YV90aW1lWywgdF9tZWFuIDo9IG1lYW4oVGltZSksIGJ5ID0gQmVuY2hdDQpkYXRhX3RpbWUgPC0gZGF0YV90aW1lWywgdF9tZWRpYW4gOj0gbWVkaWFuKFRpbWUpLCBieSA9IEJlbmNoXQ0KZGF0YV90aW1lJEJlbmNocyA8LSBkYXRhX3RpbWUkQmVuY2ggDQpsZXZlbHMoZGF0YV90aW1lJEJlbmNocykgPC0gcGFzdGUwKCJmYXN0ZXIiLCAxOjQsICI9IFsiLCBzcHJpbnRmKHBhc3RlMCgiJS4wIiwgbXlfZGlnaXRzLCAiZiIpLCBkYXRhX3RpbWVbLCBsaXN0KG1pbihUaW1lKSksIGJ5ID0gQmVuY2hdJFYxKSwgIiwgIiwgc3ByaW50ZihwYXN0ZTAoIiUuMCIsIG15X2RpZ2l0cywgImYiKSwgZGF0YV90aW1lWywgbGlzdChtYXgoVGltZSkpLCBieSA9IEJlbmNoXSRWMSksICJdLCBtZWFuPSIsIHNwcmludGYocGFzdGUwKCIlLjAiLCBteV9kaWdpdHMsICJmIiksIGRhdGFfdGltZVssIGxpc3QobWVhbihUaW1lKSksIGJ5ID0gQmVuY2hdJFYxKSwgIiwgbWVkaWFuPSIsIHNwcmludGYocGFzdGUwKCIlLjAiLCBteV9kaWdpdHMsICJmIiksIGRhdGFfdGltZVssIGxpc3QobWVkaWFuKFRpbWUpKSwgYnkgPSBCZW5jaF0kVjEpKQ0KDQpteV90aW1lIDwtIGRhdGFfdGltZVssIGxpc3QobWluKFRpbWUpLCBxdWFudGlsZShUaW1lLCBwcm9icyA9IDAuMjUpLCBtZWRpYW4oVGltZSksIHF1YW50aWxlKFRpbWUsIHByb2JzID0gMC43NSksIG1heChUaW1lKSwgbWVhbihUaW1lKSksIGJ5ID0gQmVuY2hdDQpjb2xuYW1lcyhteV90aW1lKSA8LSBjKCJGdW5jdGlvbiIsICJNaW4iLCAiMjUlIiwgIjUwJSIsICI3NSUiLCAiTWF4IiwgIk1lYW4iKQ0KbXlfdGltZSA8LSBteV90aW1lW29yZGVyKE1lYW4sIGRlY3JlYXNpbmcgPSBGQUxTRSksIF0NCnByaW50KG15X3RpbWUsIGRpZ2l0cyA9IDYpDQoNCmBgYA0KDQojIFBsb3QgUmVzdWx0cw0KDQpgYGB7ciBiZW5jaDQsIGZpZy5oZWlnaHQ9NiwgZmlnLndpZHRoPTEwfQ0KDQpnZ3Bsb3RseShnZ3Bsb3QoZGF0YSA9IGRhdGFfdGltZSwgYWVzKHggPSBUaW1lKSkgKyBnZW9tX2hpc3RvZ3JhbShhZXMoeSA9IC4uZGVuc2l0eS4uKSwgYmlucyA9IDIwLCBjb2xvciA9ICJkYXJrYmx1ZSIsIGZpbGwgPSAibGlnaHRibHVlIikgKyBmYWNldF93cmFwKH4gQmVuY2hzLCBuY29sID0gMikgKyBnZW9tX3ZsaW5lKGFlcyh4aW50ZXJjZXB0ID0gdF9tZWFuKSwgY29sb3IgPSAiYmx1ZSIsIGxpbmV0eXBlID0gImRhc2hlZCIsIHNpemUgPSAyKSArIGdlb21fdmxpbmUoYWVzKHhpbnRlcmNlcHQgPSB0X21lZGlhbiksIGNvbG9yID0gInJlZCIsIGxpbmV0eXBlID0gImRhc2hlZCIsIHNpemUgPSAyKSArIGxhYnMoeCA9ICJUaW1lIChNaWxsaXNlY29uZHMpIiwgeSA9ICJEZW5zaXR5IikgKyB0aGVtZV9idygpKQ0KDQpgYGANCg0KYGBge3IgYmVuY2g1LCBmaWcuaGVpZ2h0PTYsIGZpZy53aWR0aD0xMH0NCmdncGxvdGx5KGdncGxvdChkYXRhID0gZGF0YV90aW1lWywgLihUaW1lLCBCZW5jaCldLCBhZXMoeCA9IFRpbWUsIHkgPSAuLmNvdW50Li4sIGZpbGwgPSBCZW5jaCkpICsgZ2VvbV9oaXN0b2dyYW0oYWVzKHkgPSAuLmRlbnNpdHkuLiksIGJpbnMgPSAxMDAsIHBvc2l0aW9uID0gImZpbGwiKSArIGxhYnMoeCA9ICJUaW1lIChNaWxsaXNlY29uZHMpIiwgeSA9ICJEZW5zaXR5IikgKyB0aGVtZV9idygpKQ0KYGBgDQoNCmBgYHtyIGJlbmNoNiwgZmlnLmhlaWdodD02LCBmaWcud2lkdGg9MTB9DQpnZ3Bsb3RseShnZ3Bsb3QoZGF0YSA9IGRhdGFfdGltZVssIC4oVGltZSwgQmVuY2gpXSwgYWVzKHggPSBUaW1lLCB5ID0gLi5jb3VudC4uLCBmaWxsID0gQmVuY2gpKSArIGdlb21fZGVuc2l0eShwb3NpdGlvbiA9ICJmaWxsIikgKyBsYWJzKHggPSAiVGltZSAoTWlsbGlzZWNvbmRzKSIsIHkgPSAiRGVuc2l0eSIpICsgdGhlbWVfYncoKSkNCmBgYA0KDQojIFNjYWxpbmcgQmVuY2htYXJrcw0KDQpgYGB7ciBiZW5jaF9zY2FsZTF9DQoNCkJlbmNobWFya2VyIDwtIGZ1bmN0aW9uKGYsIHNpemUsIHJ1bnMsIGRpZ2l0cywgbmFtZSkgew0KICANCiAgZGF0YV9ydW5zIDwtIGxpc3QoKQ0KICANCiAgZm9yIChpIGluIDE6bGVuZ3RoKHNpemUpKSB7DQogICAgc2V0LnNlZWQoMTExMTEpDQogICAgZGF0YSA8LSBydW5pZihzaXplW2ldICogMTAsIDAsIDEpDQogICAgbGFiZWxzIDwtIHJvdW5kKHJ1bmlmKHNpemVbaV0sIDAsIDkpLCBkaWdpdHMgPSAwKQ0KICAgIGNhdCgiICBcbiAgXG4jIyAiLCBuYW1lLCAiIHJ1bjogIixmb3JtYXQoc2l6ZVtpXSwgYmlnLm1hcmsgPSAiLCIsIHNjaWVudGlmaWMgPSBGQUxTRSksICIgc2FtcGxlcyAoIiwgZm9ybWF0KHJ1bnNbaV0sIGJpZy5tYXJrID0gIiwiLCBzY2llbnRpZmljID0gRkFMU0UpLCAiIHRpbWVzKSAgXG4gIFxuIiwgc2VwID0gIiIpDQogICAgdGVzdF9jYXNlKGYsIHByZWRzID0gZGF0YSwgbGFiZWxzID0gbGFiZWxzLCBlcHMgPSAxZS0xNSkNCiAgICBjYXQoIiAgXG4iKQ0KICAgIGRhdGFfcnVuc1tbaV1dIDwtIHByaW50X3dlbGwobWljcm9iZW5jaG1hcmsoZihkYXRhLCBsYWJlbHMsIGVwcyA9IDFlLTE1KSwgdGltZXMgPSBydW5zW2ldKSR0aW1lLCBkaWdpdHMgPSBkaWdpdHMpDQogICAgZGF0YV9ydW5zW1tpXV0gPC0gZGF0YS50YWJsZShCZW5jaCA9IGFzLmZhY3RvcihwYXN0ZTAoIlsiLCBpLCAiXSAiLCBmb3JtYXQoc2l6ZVtpXSwgYmlnLm1hcmsgPSAiLCIsIHNjaWVudGlmaWMgPSBGQUxTRSkpKSwgRnVuY3Rpb24gPSBhcy5mYWN0b3IobmFtZSksIFRpbWUgPSBkYXRhX3J1bnNbW2ldXSkNCiAgICBnYyh2ZXJib3NlID0gRkFMU0UpDQogIH0NCiAgDQogIHJldHVybihkYXRhX3J1bnMpDQogIA0KfQ0KDQpiZW5jaF9zaXplIDwtIGMoMTAwLCAxMDAwLCAxMDAwMCwgMTAwMDAwLCAxMDAwMDAwLCAxMDAwMDAwMCkNCmJlbmNoX3J1bnMgPC0gYygxMDAwMCwgNTAwMCwgMTAwMCwgNTAwLCAxMDAsIDUwKQ0KDQpgYGANCg0KYGBge3IgYmVuY2hfc2NhbGUyLCByZXN1bHRzPSJhc2lzIn0NCg0KcnVuMSA8LSBCZW5jaG1hcmtlcihmYXN0ZXIzLCBiZW5jaF9zaXplLCBiZW5jaF9ydW5zLCBteV9kaWdpdHMsICJQdXJlIFIiKQ0KcnVuMiA8LSBCZW5jaG1hcmtlcihmYXN0ZXI0LCBiZW5jaF9zaXplLCBiZW5jaF9ydW5zLCBteV9kaWdpdHMsICJSY3BwIikNCg0KYGBgDQoNCiMgU2NhbGluZyBSZXN1bHRzDQoNCmBgYHtyIGJlbmNoX3NjYWxlM30NCg0KcnVuMV9hbGwgPC0gcmJpbmRsaXN0KHJ1bjEpDQpydW4yX2FsbCA8LSByYmluZGxpc3QocnVuMikNCnJ1bl9hbGwgPC0gcmJpbmQocnVuMV9hbGwsIHJ1bjJfYWxsKQ0KcnVuX2FsbCRSZXBlYXRzIDwtIHJlcChpbnZlcnNlLnJsZShsaXN0KGxlbmd0aHMgPSBiZW5jaF9ydW5zLCB2YWx1ZXMgPSBiZW5jaF9zaXplKSksIDIpDQpydW5fYWxsJE1pbE9icyA8LSAoMTAwMCAvIHJ1bl9hbGwkVGltZSkgKiBydW5fYWxsJFJlcGVhdHMgLyAxMDAwMDAwDQpydW5fdGltZSA8LSBydW5fYWxsWywgbGlzdChxdWFudGlsZShUaW1lLCBwcm9icyA9IDAuMDUpLCBtZWRpYW4oVGltZSksIHF1YW50aWxlKFRpbWUsIHByb2JzID0gMC45NSksIG1lYW4oVGltZSkpLCBieSA9IGxpc3QoRnVuY3Rpb24sIEJlbmNoKV0NCmNvbG5hbWVzKHJ1bl90aW1lKSA8LSBjKCJGdW5jdGlvbiIsICJCZW5jaG1hcmsiLCAiNSUiLCAiNTAlIiwgIjk1JSIsICJNZWFuIikNCnJ1bl90aW1lJGBNaWwuT2JzL3NgIDwtICgxMDAwIC8gcnVuX3RpbWUkTWVhbikgKiBiZW5jaF9zaXplIC8gMTAwMDAwMA0KcnVuX3RpbWUkYDUlYCA8LSBmb3JtYXQocnVuX3RpbWUkYDUlYCwgZGlnaXRzID0gNiwgc2NpZW50aWZpYyA9IEZBTFNFKQ0KcnVuX3RpbWUkYDUwJWAgPC0gZm9ybWF0KHJ1bl90aW1lJGA1MCVgLCBkaWdpdHMgPSA2LCBzY2llbnRpZmljID0gRkFMU0UpDQpydW5fdGltZSRgOTUlYCA8LSBmb3JtYXQocnVuX3RpbWUkYDk1JWAsIGRpZ2l0cyA9IDYsIHNjaWVudGlmaWMgPSBGQUxTRSkNCnJ1bl90aW1lJE1lYW4gPC0gZm9ybWF0KHJ1bl90aW1lJE1lYW4sIGRpZ2l0cyA9IDYsIHNjaWVudGlmaWMgPSBGQUxTRSkNCnJ1bl90aW1lJGBNaWwuT2JzL3NgIDwtIGZvcm1hdChydW5fdGltZSRgTWlsLk9icy9zYCwgZGlnaXRzID0gNiwgc2NpZW50aWZpYyA9IEZBTFNFKQ0KDQpwcmludChydW5fdGltZVsxOihucm93KHJ1bl90aW1lKSAvIDIpXSkNCnByaW50KHJ1bl90aW1lWyhucm93KHJ1bl90aW1lKSAvIDIgKyAxKTpucm93KHJ1bl90aW1lKV0pDQoNCmBgYA0KDQpgYGB7ciBiZW5jaF9zY2FsZTQsIGZpZy5oZWlnaHQ9NiwgZmlnLndpZHRoPTEwfQ0KDQpnZ3Bsb3QocnVuX2FsbCwgYWVzKHggPSBCZW5jaCwgeSA9IFRpbWUsIGNvbG9yID0gRnVuY3Rpb24sIGZpbGwgPSBCZW5jaCkpICsgZ2VvbV9ib3hwbG90KCkgKyBzY2FsZV95X2xvZzEwKGxhYmVscyA9IHNjYWxlczo6Y29tbWEsIGJyZWFrcyA9IGMoMC4wMSwgMC4xLCAxLCAxMCwgMTAwLCAxMDAwLCAxMDAwMCkpICsgc3RhdF9zdW1tYXJ5KGZ1bi55ID0gbWVhbiwgZ2VvbSA9ICJsaW5lIiwgYWVzKGdyb3VwID0gRnVuY3Rpb24pKSArIHN0YXRfc3VtbWFyeShmdW4ueSA9IG1lYW4sIGdlb20gPSAicG9pbnQiLCBhZXMoZ3JvdXAgPSBGdW5jdGlvbikpICsgbGFicyh4ID0gIkJlbmNobWFyayIsIHkgPSAiVGltZSAoTWlsbGlzZWNvbmRzKSIpICsgdGhlbWVfYncoKQ0KDQpgYGANCg0KYGBge3IgYmVuY2hfc2NhbGU1LCBmaWcuaGVpZ2h0PTYsIGZpZy53aWR0aD0xMH0NCg0KZ2dwbG90KHJ1bl9hbGwsIGFlcyh4ID0gQmVuY2gsIHkgPSBNaWxPYnMsIGNvbG9yID0gRnVuY3Rpb24sIGZpbGwgPSBCZW5jaCkpICsgZ2VvbV9ib3hwbG90KCkgKyBzY2FsZV95X2xvZzEwKGxhYmVscyA9IHNjYWxlczo6Y29tbWEsIGJyZWFrcyA9IGMoMSwgMi41LCA1LCA3LjUsIDEwLCAxMi41LCAxNSwgMTcuNSwgMjAsIDIyLjUsIDI1KSwgbGltaXRzID0gYygxLCBOQSkpICsgc3RhdF9zdW1tYXJ5KGZ1bi55ID0gbWVhbiwgZ2VvbSA9ICJsaW5lIiwgYWVzKGdyb3VwID0gRnVuY3Rpb24pKSArIHN0YXRfc3VtbWFyeShmdW4ueSA9IG1lYW4sIGdlb20gPSAicG9pbnQiLCBhZXMoZ3JvdXAgPSBGdW5jdGlvbikpICsgbGFicyh4ID0gIkJlbmNobWFyayIsIHkgPSAiVGhyb3VnaHB1dCAoTWlsbGlvbiBPYnMuL3MpIikgKyB0aGVtZV9idygpDQoNCmBgYA0KDQpgYGB7ciBiZW5jaF9zY2FsZTYsIGZpZy5oZWlnaHQ9NiwgZmlnLndpZHRoPTEwfQ0KDQpnZ3Bsb3QoZGF0YSA9IHJ1bl9hbGwsIGFlcyh4ID0gTWlsT2JzLCBjb2xvciA9IEZ1bmN0aW9uLCBmaWxsID0gRnVuY3Rpb24sIGdyb3VwID0gRnVuY3Rpb24pKSArIGNvb3JkX2ZsaXAoKSArIHN0YXRfZWNkZihhZXMoeW1pbiA9IC4ueS4uLCB5bWF4ID0gMSksIGFscGhhID0gMC41LCBnZW9tID0gInJpYmJvbiIpICsgc3RhdF9lY2RmKGdlb20gPSAibGluZSIsIHNpemUgPSAyLCBhbHBoYSA9IDAuNzUsIHBhZCA9IEZBTFNFKSArIGxhYnMoeCA9ICJUaHJvdWdocHV0IChNaWxsaW9uIE9icy4vcykiLCB5ID0gIlBlcmNlbnRpbGUiKSArIGZhY2V0X3dyYXAofiBCZW5jaCwgZGlyID0gImgiLCBuY29sID0gMiwgc2NhbGVzID0gImZyZWUiKSArIHRoZW1lX2J3KCkNCg0KYGBgDQo=